Vue 元件之間的核心溝通方式有三種:
props
(父 → 子):父層把資料當作 唯讀 來源傳給子層,子層 不可
直接改 prop
(單向數據流)。emit
(子 → 父):子層告訴父層「發生了什麼事」或「我想改某個值」,由父層決定要不要改資料。v-model
(雙向):其實是「props
+ emit
的語法糖」,標準事件名是 update:modelValu
e(或使用具名 v-model
)。這三者組合起來,就是 單向數據流 (one-way data flow) 的基礎。
props
:父傳子(唯讀),定義、接收與使用<script setup lang="ts">
import { withDefaults, defineProps, toRefs } from 'vue'
const props = withDefaults(defineProps<{
title: string
count?: number
user?: { id: number; name: string }
}>(), {
title: '預設標題',
count: 0
})
const { title, count, user } = toRefs(props)
</script>
<template>
<h3>{{ title }}</h3>
<p>count: {{ count }}</p>
<p v-if="user">user: {{ user.name }}</p>
</template>
props
引用子元件 <ChildA>
:title="title"
→ 將父元件的響應式變數 title
傳入子元件的 props.title
:count="count"
→ 把數字 5 傳下去 (如果父層更新 count
,子層會同步更新)。:user="user"
→ 傳入一個物件 { id: 1, name: "kuku" }
給子元件。
<script setup lang="ts">
import { ref } from 'vue'
import ChildA from './ChildA.vue'
const title = ref('Hello Props')
const count = ref(5)
const user = ref({ id: 1, name: 'kuku' })
</script>
<template>
<ChildA :title="title" :count="count" :user="user" />
</template>
在
<template>
綁定ref
時,Vue 會自動幫你解包.value
,所以傳給子元件的其實是title.value
、count.value
、user.value
,而不是ref
本身。
props
的重點 & 常見坑props
在子元件是 唯讀(shallow readonly)。直接 props.count++
會在開發模式下警告。vendor.js:600 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop’s value.
vendor.js:600 [Vue warn]:避免直接修改 prop,因為每次父元件重新渲染時,其值都會被覆寫。建議使用基於 prop 值的資料屬性或計算屬性。
巢狀物件變更:props.user.name = 'X'
在技術上改得動(因為淺只讀),但屬於 反模式,應改用 emit
通知父層更新。
解構陷阱:const { title } = props
會丟失響應式,請用 toRefs(props)
或直接用 props.title
。
emit
:往父層發事件(回報狀態/請求修改)以下是一個組件 ChildB
<script setup lang="ts">
const emit = defineEmits<{
(e: 'select', id: number): void
(e: 'save', payload: { id: number; name: string }): void
}>()
function onSelect() { emit('select', 42) }
function onSave() { emit('save', { id: 1, name: 'New Name' }) }
</script>
<template>
<button @click="onSelect">Select #42</button>
<button @click="onSave">Save</button>
</template>
在父層 @
是 v-on
的縮寫,用來監聽子元件透過 emit
發出的事件。
以下程式碼 @select
代表監聽 ChildB 裡觸發的 "select" 事件,也就是當子元件執行 emit('select', someId)
時,會觸發這裡的箭頭函式。
同理,@save
監聽 ChildB 的 "save" 事件,當子元件呼叫 emit('save', somePayload) 時,會觸發這裡的函式。
<ChildB @select="id => console.log('id:', id)"
@save="p => console.log('payload:', p)" />
emit
命名建議submit
、select
、change
。update:xxx
v-model
(雙向綁定)= props
+ emit
的語法糖v-model
(modelValue
)定義一個 Counter
組件
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{ modelValue: number }>() // 宣告父層傳進來的 props
const emit = defineEmits<{ (e: 'update:modelValue', v: number): void }>() // 宣告子元件可以觸發的事件(emit)
const count = computed({
// 回傳父層的 props.modelValue,讓畫面能顯示父層傳進來的值
get: () => props.modelValue,
// 對 count 賦值時,就呼叫 emit('update:modelValue', v),通知父層更新
set: v => emit('update:modelValue', v)
})
</script>
<template>
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</template>
父層就能用:
<Counter v-model="count" />
v-model
(多個雙向值)<Range v-model:min="min" v-model:max="max" />
defineModel
簡寫Vue 3.4 + defineModel
簡寫 ===「在子層自動生成對應的 props
& emit
」。
以下是一個計數器,使用 defineModel
自動生成一個 prop:modelValue
,型別是 number
,預設值為 0,同時自動生成一個 emit:update:modelValue
。
透過 count
使用時,不用手動寫 getter / setter,因為它已經是雙向響應式的。
<!-- CounterModel.vue -->
<script setup lang="ts">
const count = defineModel<number>({ default: 0 }) // 對應 v-model
</script>
<template>
<button @click="count--">-</button>
<span class="mx-2">{{ count }}</span>
<button @click="count++">+</button>
</template>
接下來一樣使用 defineModel
做多個 v-model
:
defineModel<number>('min', { default: 0 })
-> 自動生成 props.min
,型別 number
,預設值 0,以及 emit('update:min', v)
defineModel<number>('max', { default: 100 })
-> 也會自動生成 props.max
,型別 number
,預設值 100,emit('update:max', v)
在 input
中透過 v-model:number
語法糖,雙向綁定 min
<!-- RangeModel.vue -->
<script setup lang="ts">
const min = defineModel<number>('min', { default: 0 })
const max = defineModel<number>('max', { default: 100 })
</script>
<template>
<input type="number" v-model.number="min" />
<input type="number" v-model.number="max" />
</template>
以上兩個組件就可以在父層就可以做雙向綁定
<script setup lang="ts">
import CounterModel from './CounterModel.vue'
import RangeModel from './RangeModel.vue'
const myCount = ref(5)
const myMin = ref(10)
const myMax = ref(90)
</script>
<template>
<CounterModel v-model="myCount" />
<p>父層 count: {{ myCount }}</p>
<RangeModel v-model:min="myMin" v-model:max="myMax" />
<p>父層範圍: {{ myMin }} - {{ myMax }}</p>
</template>
父狀態 → (props) → 子元件(唯讀 readonly)
↑
(emit)
父層是單一真相來源。
子層若要暫存修改(例如表單),要先做本地副本,最後再 emit
更新。
// ChildEdit.vue(子)——本地可編輯副本
import { ref, watch } from 'vue'
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ (e:'update:modelValue', v:string): void }>()
const draft = ref(props.modelValue)
// 父層更新時同步本地值
watch(() => props.modelValue, v => { draft.value = v })
function save() {
emit('update:modelValue', draft.value)
}
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{
(e:'update:modelValue', v:string): void
(e:'submit', v:string): void
}>()
const keyword = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})
function onSubmit() { emit('submit', keyword.value.trim()) }
</script>
<template>
<div>
<input v-model="keyword" placeholder="輸入關鍵字…" />
<button @click="onSubmit">搜尋</button>
</div>
</template>
父層:
<SearchInput v-model="kw" @submit="v => console.log('Search:', v)" />
props:父層要給子層顯示或計算所需的資料、或調整子層行為的設定值(如 disabled
、size
、columns
)。
emit:子層 通知父層事件(如 submit
/select
),或提出 我想改某個值 的請求(搭配 update:*
或 v-model
)。
v-model:當子層看起來像「一個可控表單或控件」時(input、select、dialog 的 open
等),用 v-model
最直覺,之後我們會更一步說明 v-model
在 Vue3.5 的進化。
單向數據流 保證狀態一致性,父層永遠是唯一來源。